﻿using System;
using System.Linq;
using System.Text;
using System.Web.UI.WebControls;
using System.Web.UI;
using System.Web;
using System.ComponentModel;
using System.Collections;
using System.Reflection;
using System.Web.UI.WebControls.WebParts;

namespace AsyncContentPlaceHolder
{
    [ParseChildren(true)]
    public class AsyncContentPlaceHolder : ContentPlaceHolder
    {
        #region Member variables

        private UpdateProgress _upg;
        private UpdatePanel _up;
        private Timer _t;
        private bool _firstRun = true;
        private bool _disabled = false;

        internal const string UpdateProgressID = "UpdateProgress";
        internal const string UpdatePanelID = "UpdatePanel";
        internal const string TimerID = "Timer";
        internal const string ScriptManagerID = "ScriptManager";

        private int _delayContentLoading = 0;
        private bool _childrenAsTriggers = true;
        private UpdatePanelTriggerCollection _triggers;
        private ITemplate _progressTemplate;
        private bool _registerScriptManager = false;
        private string _disableAsyncLoadingIfUrlContains = "";
        private bool _registerJQuery = true;

        #endregion

        #region Properties

        /// <summary>
        /// The delay in milliseconds for the place holder content to start loading after the page load.
        /// </summary>
        /// <value>
        /// Delay in milliseconds
        /// </value>
        public int DelayContentLoading
        {
            get { return _delayContentLoading; }

            set
            {
                if (value < 0)
                    _delayContentLoading = 0;
                else
                    _delayContentLoading = value;
            }
        }

        /// <summary>
        /// Allow child controls to trigger an async page refresh.
        /// </summary>
        /// <value>
        ///   <c>true</c> if [children as triggers]; otherwise, <c>false</c>.
        /// </value>
        public bool ChildrenAsTriggers
        {
            get { return _childrenAsTriggers; }
            set { _childrenAsTriggers = value; }
        }

        /// <summary>
        /// Gets the trigger collection.
        /// </summary>
        [PersistenceMode(PersistenceMode.InnerProperty)]
        public UpdatePanelTriggerCollection Triggers
        {
            get
            {
                if (_triggers == null)
                    _triggers = new UpdatePanelTriggerCollection(_up);

                return this._triggers;
            }
        }

        /// <summary>
        /// Gets or sets the progress template.
        /// </summary>
        /// <value>
        /// The progress template.
        /// </value>
        [PersistenceMode(PersistenceMode.InnerProperty)]
        public ITemplate ProgressTemplate
        {
            get { return _progressTemplate; }
            set { _progressTemplate = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether [register script manager].
        /// </summary>
        /// <value>
        /// <c>true</c> if [register script manager]; otherwise, <c>false</c>.
        /// </value>
        public bool RegisterScriptManager
        {
            get { return _registerScriptManager; }
            set { _registerScriptManager = value; }
        }

        /// <summary>
        /// Gets or sets the disable async loading if URL contains.
        /// </summary>
        /// <value>
        /// The disable async loading if URL contains.
        /// </value>
        public string DisableAsyncLoadingIfUrlContains
        {
            get { return _disableAsyncLoadingIfUrlContains; }

            set
            {
                if (value != null)
                    _disableAsyncLoadingIfUrlContains = value;
                else
                    _disableAsyncLoadingIfUrlContains = "";
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether [register J query].
        /// </summary>
        /// <value>
        ///   <c>true</c> if [register J query]; otherwise, <c>false</c>.
        /// </value>
        public bool RegisterJQuery
        {
            get { return _registerJQuery; }
            set { _registerJQuery = value; }
        }

        #endregion

        /// <summary>
        /// Gets a <see cref="T:System.Web.UI.ControlCollection"/> object that represents the child controls for a specified server control in the UI hierarchy.
        /// </summary>
        /// <returns>
        /// The collection of child controls for the specified server control.
        ///   </returns>
        public override ControlCollection Controls
        {
            get
            {
                //Check if it's the first time the controls collection is accessed
                if (_firstRun)
                {
                    //Check if page is in the disabled list
                    string[] disableAsyncLoadingIfUrlContains = DisableAsyncLoadingIfUrlContains.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                    foreach (string s in disableAsyncLoadingIfUrlContains)
                    {
                        if (HttpContext.Current.Request.Url.OriginalString.Contains(s))
                        {
                            _disabled = true;
                            break;
                        }
                    }
                }

                //If the update panel functionality is not required, return the original control collection
                if (_disabled)
                {
                    _firstRun = false;

                    return base.Controls;
                }

                //Check if update panel has been initialized
                if (_up == null)
                {
                    ControlCollection cc = base.Controls;

                    //Register script maneger if requested
                    if (RegisterScriptManager)
                        cc.Add(new ScriptManager()
                        {
                            ID = ScriptManagerID,
                            EnablePartialRendering = true
                        });

                    //Add an update progress control if a template was specified
                    if (ProgressTemplate != null)
                    {
                        _upg = new UpdateProgress()
                        {
                            ID = UpdateProgressID,
                            AssociatedUpdatePanelID = UpdatePanelID,
                            ProgressTemplate = this.ProgressTemplate
                        };

                        cc.Add(_upg);
                    }

                    //Add the update panel
                    _up = new UpdatePanel()
                    {
                        ID = UpdatePanelID,
                        ChildrenAsTriggers = this.ChildrenAsTriggers,
                        UpdateMode = UpdatePanelUpdateMode.Conditional
                    };

                    cc.Add(_up);

                    //Add a timer for delayed content loading if the parameter was specified
                    if (DelayContentLoading > 0)
                    {
                        _t = new Timer()
                        {
                            ID = TimerID,
                            Interval = DelayContentLoading
                        };

                        _t.Tick += new EventHandler<EventArgs>(_t_Tick);

                        //Add timer inside the update panel
                        _up.ContentTemplateContainer.Controls.Add(_t);

                        //Register timer control as trigger (if children are not registered triggers)
                        if (!ChildrenAsTriggers)
                        {
                            _up.Triggers.Add(new AsyncPostBackTrigger()
                            {
                                ControlID = TimerID,
                                EventName = "Tick"
                            });
                        }
                    }
                }

                return _up.ContentTemplateContainer.Controls;
            }
        }

        /// <summary>
        /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
        protected override void OnInit(EventArgs e)
        {
            if (_up != null)
            {
                bool externalTriggerFixRegistered = false;

                //Register triggers
                if (Triggers.Count > 0)
                    EnsureChildControls();

                foreach (UpdatePanelTrigger upt in Triggers)
                {
                    Control c;

                    if (upt.GetType() == typeof(PostBackTrigger))
                    {
                        //Add post back trigger if the corresponding control can be found
                        c = FindControl(((PostBackTrigger)upt).ControlID, null);

                        if (c != null)
                            _up.Triggers.Add(new PostBackTrigger()
                            {
                                ControlID = c.UniqueID
                            });
                    }
                    else
                    {
                        if (upt.GetType() == typeof(AsyncPostBackTrigger))
                        {
                            //Add async post back trigger if the corresponding control can be found
                            AsyncPostBackTrigger apbt = (AsyncPostBackTrigger)upt;
                            c = FindControl(apbt.ControlID, null);

                            if (c != null)
                            {
                                _up.Triggers.Add(new AsyncPostBackTrigger()
                                {
                                    ControlID = c.UniqueID,
                                    EventName = apbt.EventName
                                });

                                //Register async trigger fix if a progress template was specified
                                if (ProgressTemplate != null)
                                {
                                    //Ignore our timer post back trigger
                                    if (apbt.ControlID != TimerID && apbt.EventName != "Tick")
                                    {
                                        //Register script
                                        if (!externalTriggerFixRegistered)
                                        {
                                            RegisterExternalTriggerFixForUpdateProgress();
                                            externalTriggerFixRegistered = true;
                                        }

                                        //Register async trigger
                                        RegisterExternalAsyncTrigger(c);
                                    }
                                }
                            }
                        }
                    }
                }

                //Add jquery reference to page header (embedded resource file)
                if (RegisterJQuery)
                    Page.Header.Controls.Add(new Literal()
                    {
                        Mode = LiteralMode.PassThrough,
                        Text = String.Format("<script type='text/javascript' src='{0}'></script>", Page.ClientScript.GetWebResourceUrl(this.GetType(), "AsyncContentPlaceHolder.jquery-1.4.1.js"))
                    });
            }

            base.OnInit(e);
        }

        /// <summary>
        /// Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparation for posting back or rendering.
        /// </summary>
        protected override void CreateChildControls()
        {
            if (_up != null)
            {
                //On the first page load, if the parameter is specified, remove all child controls exept for the first (timer control)
                if (DelayContentLoading > 0 && !Page.IsPostBack)
                {
                    for (int i = _up.ContentTemplateContainer.Controls.Count - 1; i > 0; i--)
                    {
                        try
                        {
                            _up.ContentTemplateContainer.Controls.RemoveAt(i);
                        }
                        catch
                        {
                            //Not all controls like being removed...
                        }
                    }
                }

                //Disable timer if another control posts back (unable to check if page is a post back in the Controls property)
                if (_t != null && Page.Request.Params["__EVENTTARGET"] != null && !((string)Page.Request.Params["__EVENTTARGET"]).Equals(_t.UniqueID))
                    _t.Enabled = false;
            }

            base.CreateChildControls();
        }

        /// <summary>
        /// Raises the <see cref="E:System.Web.UI.Control.PreRender" /> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs" /> object that contains the event data.</param>
        protected override void OnPreRender(EventArgs e)
        {
            if (_up != null)
            {
                //Add javascript to page to execute all javascript located within script tags in the async content placeholder
                //Script code generously provided by Tom Heylen
                string updatePanelClientId = _up.ClientID;
                string scriptKey = String.Concat("AsyncContentPlaceHolderExecuteScriptTags_", updatePanelClientId);

                //Add script to page
                if (!Page.ClientScript.IsClientScriptBlockRegistered(scriptKey))
                {
                    string script = String.Format(@"$(document).ready(function () {{
                                                    Sys.WebForms.PageRequestManager.getInstance().add_endRequest(EndRequestHandler);

                                                    function EndRequestHandler(sender, args) {{
                                                        if (args.get_error() == undefined) {{
                                                            $('div[id=""{1}""] script[type=""text/javascript""]').each(function (data) {{
                                                                if ($(this).attr('src') == undefined && $(this).html().length > 0) {{
                                                                    eval($(this).html());
                                                                }}
                                                            }});
                                                        }}
                                                    }}
                                                }});", Page.ClientScript.GetWebResourceUrl(this.GetType(), "AsyncContentPlaceHolder.jquery-1.4.1.js"), updatePanelClientId);

                    Page.ClientScript.RegisterClientScriptBlock(this.GetType(), scriptKey, script, true);
                }
            }

            base.OnPreRender(e);
        }

        /// <summary>
        /// Handles the Tick event of the t control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void _t_Tick(object sender, EventArgs e)
        {
            //Disable the timer after postback
            ((Timer)sender).Enabled = false;
        }

        /// <summary>
        /// Registers the external trigger fix for update progress.
        /// </summary>
        private void RegisterExternalTriggerFixForUpdateProgress()
        {
            //By default the update progress control doesn't show when using an external async trigger
            //Code copied from: http://www.brothersincode.com/post/AsyncPostBackTrigger-On-A-Control-External-To-An-Update-Panel-Doesnt-Display-An-Update-Progress-Control.aspx
            string updateProgressWTriggerFix = @"var triggerMappings = new Array();

                                                  function TriggerMapping(controlId, updatePanelId)
                                                    {
                                                      this.TriggerControlId = controlId;
                                                      this.UpdatePanelId = updatePanelId;
                                                    }

                                                    function RegisterTriggerMapping(controlId, updatePanelId)
                                                    {
                                                      triggerMappings.push(new TriggerMapping(controlId, updatePanelId));
                                                    }

                                                    function GetTriggerMapping(control)
                                                    {
                                                      for(var i=0; i<triggerMappings.length; i++)
                                                      {
                                                        if(triggerMappings[i].TriggerControlId == control.id)
                                                        {
                                                          return triggerMappings[i];
                                                        }
                                                      }
                                                      return null;
                                                    }

                                                    var prm = Sys.WebForms.PageRequestManager.getInstance();
                                                    function CancelAsyncPostBack() {
                                                        if (prm.get_isInAsyncPostBack()) {
                                                          prm.abortPostBack();
                                                        }
                                                    }


                                                    prm.add_initializeRequest(InitializeRequest);
                                                    prm.add_endRequest(EndRequest);
                                                    var postBackElement;
                                                    function InitializeRequest(sender, args) {
                                                        if (prm.get_isInAsyncPostBack()) {
                                                            args.set_cancel(true);
                                                        }
                                                        postBackElement = args.get_postBackElement();
       
                                                        var triggerMapping = GetTriggerMapping(postBackElement);
                                                        if (triggerMapping != null) {
                                                            $get(triggerMapping.UpdatePanelId).style.display = 'block';
                                                        }
                                                    }

                                                    function EndRequest(sender, args) {
                                                        var triggerMapping = GetTriggerMapping(postBackElement);
                                                        if (triggerMapping != null) {
                                                            $get(triggerMapping.UpdatePanelId).style.display = 'none';
                                                        }
                                                    }";

            this.Page.ClientScript.RegisterStartupScript(this.GetType(), "UpdateProgressWTriggerFix", updateProgressWTriggerFix, true);
        }

        /// <summary>
        /// Registers the external async trigger.
        /// </summary>
        /// <param name="triggerControl">The trigger control.</param>
        private void RegisterExternalAsyncTrigger(Control triggerControl)
        {
            if (triggerControl != null && _upg != null)
                this.Page.ClientScript.RegisterStartupScript(this.GetType(), triggerControl.ClientID, String.Format("RegisterTriggerMapping('{0}','{1}');", triggerControl.ClientID, _upg.ClientID), true);
        }

        /// <summary>
        /// Finds the control in parent or above.
        /// </summary>
        /// <param name="controlID">The control ID.</param>
        /// <param name="parentControl">The parent control.</param>
        /// <returns></returns>
        private Control FindControl(string controlID, Control parentControl)
        {
            Control returnValue = null;

            if (parentControl == null)
                parentControl = Page.Master != null ? (Control)Page.Master : (Control)Page;

            //Find the control without invoking the Control.FindControl method
            foreach (Control c in parentControl.Controls)
            {
                if (c.ID == controlID)
                    return c;

                //Check if the control is a web part zone, if so loop all web parts
                if (c.GetType().IsSubclassOf(typeof(WebPartZone)))
                {
                    foreach (WebPart wp in ((WebPartZone)c).WebParts)
                    {
                        if (wp.ID == controlID)
                            return wp;

                        foreach (Control wpc in wp.Controls)
                        {
                            if (wpc.ID == controlID)
                                return wpc;

                            returnValue = FindControl(controlID, wpc);

                            if (returnValue != null)
                                break;
                        }

                        if (returnValue != null)
                            break;
                    }
                }

                if (returnValue == null)
                    returnValue = FindControl(controlID, c);

                if (returnValue != null)
                    break;
            }

            return returnValue;
        }

        /// <summary>
        /// Should the place holder child controls be loaded?
        /// </summary>
        /// <param name="page">The page.</param>
        /// <param name="asyncContentPlaceHolderID">The async content place holder ID.</param>
        /// <param name="childControl">The child control.</param>
        /// <returns></returns>
        public static bool LoadPlaceHolderChildControls(Page page, string asyncContentPlaceHolderID, Control childControl)
        {
            if (page != null && !String.IsNullOrEmpty(asyncContentPlaceHolderID) && childControl != null)
            {
                //Find async post back control on the masterpage and check whether content should be loaded
                if (page.Request.Params["__EVENTTARGET"] == null && page.Master != null)
                {
                    AsyncContentPlaceHolder acph = null;

                    try
                    {
                        acph = (AsyncContentPlaceHolder)page.Master.FindControl(asyncContentPlaceHolderID);
                    }
                    catch (InvalidCastException)
                    {

                    }

                    //Check if delayed content loading is enabled on the content place holder and if the control belongs to the content place holder
                    if (acph != null)
                        return acph.DelayContentLoading == 0 || acph._disabled || acph.FindControl(childControl.ID, acph) == null;
                }
            }

            return true;
        }

        /// <summary>
        /// Gets the update panel.
        /// </summary>
        /// <param name="page">The page.</param>
        /// <param name="asyncContentPlaceHolderID">The async content place holder ID.</param>
        /// <returns></returns>
        public static UpdatePanel GetUpdatePanel(Page page, string asyncContentPlaceHolderID)
        {
            //Find the async post back control and return an instance of the update panel
            if (page.Master != null)
            {
                AsyncContentPlaceHolder acph = null;

                try
                {
                    acph = (AsyncContentPlaceHolder)page.Master.FindControl(asyncContentPlaceHolderID);
                }
                catch (InvalidCastException)
                {

                }

                //Check if delayed content loading is enabled on the content place holder and if the control belongs to the content place holder
                if (acph != null)
                    return acph._up;
            }

            return null;
        }
    }
}